Clasificación clientes campaña bancaria
Modelo predictivo para clasificar los clientes de un banco Portugues¶
Notebook por Alfredo Pasmiño¶
Basado en el problema de clasificación de clientes de una campaña de marketing telefónico de un banco portugues, realizaremos un modelo para clasificar correctamente la variable a precir si el cliente acepta o no el producto que se le ofrece.
Más información https://archive.ics.uci.edu/ml/datasets/bank+marketing
1 Librerías¶
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib import cm as cm
import seaborn as sns
from sklearn.preprocessing import LabelEncoder, OneHotEncoder, StandardScaler
from sklearn.naive_bayes import GaussianNB
from sklearn.utils import resample
from sklearn.model_selection import (cross_val_score, cross_val_predict, RandomizedSearchCV, ShuffleSplit,
KFold, StratifiedKFold)
from sklearn.metrics import (confusion_matrix, precision_recall_fscore_support, mean_squared_error, roc_curve, auc,
classification_report, accuracy_score, make_scorer, precision_recall_curve,recall_score,
f1_score)
from sklearn.linear_model import LogisticRegression
from sklearn import linear_model, datasets, cross_validation, metrics
from xgboost import XGBClassifier
from sklearn.model_selection import learning_curve
from scipy import interp
2. Análisis Exploratorio¶
Como primer paso Leemos el archivo con los datos, verificamos el tipo de dato de cada variable y visualizamos algunos estadísticas básicas del dataframe. para ello leerremos un archivo en formato txt como primera aproximación al modelo final en productivo.
#ruta del archivo con los datos
ruta="C:/Users/alfredo/Documents/Diplomado"
df=pd.read_csv(ruta+"/bank-full.csv", sep=";", decimal=".", header=0)
#ruta del archivo con los datos
df.head()
#verificamos el número de filas y columnas
df.shape
El dataframe contiene 45.211 filas y 17 columnas
#verificamos los tipos de datos
df.info()
df.describe()
#revisamos si existen missing
df.isnull().sum().sum()
El dataframe no tiene datos missing
%matplotlib inline
sns.set()
df[df.dtypes[(df.dtypes=="float64")|(df.dtypes=="int64")].index.values].hist(figsize=[14,14])
Revisamos los histogramas y podemos verificar cada una de las variables, su distribución y si presenta outliers en cada variable.
#graficamos las variables categóricas
fig, axes =plt.subplots(4,3, figsize=(12,12), sharex=True)
axes = axes.flatten()
object_bol = df.dtypes == 'object'
for ax, catplot in zip(axes, df.dtypes[object_bol].index):
sns.countplot(y=catplot, data=df, ax=ax)
plt.tight_layout()
plt.show()
#correlación
df.corr()
#matriz de correlación
plt.rcParams['figure.figsize'] = (15.0, 6.0)
corr=df[['age','balance','day','duration','campaign','pdays','previous']].corr()
cmap = cm.get_cmap('seismic', 50)
sns.heatmap(corr, linewidths=.5, cmap=cmap)
Revisamos la correlación entre variables y verificamos que las variables no estan correlacionadas, la única que tiene una correlación positiva pero debil es "Previous" y "Pday".
#graficamos la poporción de variable a predecir
print(df['y'].value_counts())
plt.figure(figsize=(8,5))
ax = sns.countplot(x='y', data=df)
Volvemos a revisar la variable a predecir "Y" y podemos verificar que tenemos un problema de desbalance por lo que nuestro modelo no será muy bueno al precir los clientes que si aceptaron el producto, para ellos utilizaremos un resampling para que nustra proporción de "si" y "no" sea lo mas parejo posible.
3. Feature engineering¶
#label encoder
le=LabelEncoder()
for col in df[['default', 'housing', 'loan', 'y']]:
data=df[col].append(df[col])
le.fit(data.values)
df[col]=le.transform(df[col])
para las variables categóricas con solo 2 tipos de categorías codificamos sus variables y revisamos el resultado.
#revisamos el resultado
df[['default', 'housing', 'loan', 'y']].head()
Para las variables categóricas con mutiples categorias aplicamos one hot encoding por lo que nuestro número de columnas aumenta a 49, revisaremos como quedan en la siguiente muestra.
#one hot encoder
col_transformar=['job', 'marital', 'education', 'contact', 'month', 'poutcome']
df = pd.get_dummies(df, columns = col_transformar, sparse=True)
df.shape
df.head()
Artificialmente generaremos muestras para la categoría minoritaria para balancear la variable a predecir
#separamos la clase mayoritaria y minoritaria
df_majority = df[df.y==0]
df_minority = df[df.y==1]
#generamos más datos artificialmente para la clase minoritaria
df_minority_upsampled = resample(df_minority,
replace=True,
n_samples=39922,
random_state=123)
#combinamos la clase minoritaria con la generada mayoritaria
df_upsampled = pd.concat([df_majority, df_minority_upsampled])
# mostramos la nueva clase
print(df_upsampled.y.value_counts())
#selecciono las variables para construir el modelo
y = df_upsampled.y
X = df_upsampled.drop('y', axis=1)
Como tenemos variables con diferentes unidades de medida estandarizaremos las variables.
#estandarizo las variables
seed = 10
scaler = StandardScaler()
scaler.fit(X)
X =scaler.transform(X)
4. Funciones¶
Creamos algunas funciones más complejas con nuestro dataset y que nos seviran más adelante.
La función plot_confusionMatrix recibe como parámetro de ingreso la variable "target" clasificadas en el modelo y la variable "prediction" que son las etiquetas en la muestra de testing.
def plot_confusionMatrix(targets, predictions, target_names=['No acepta', 'Acepta'], cmap="YlGnBu"):
"""
Función que grafica la matriz de confusión
"""
cm = confusion_matrix(targets, predictions)
dfcm = pd.DataFrame(data=cm, columns=target_names, index=target_names)
plt.figure(figsize=(10,6))
plt.title('Matriz de confusión')
sns.heatmap(dfcm, annot=True, fmt="d", linewidths=.5, cmap=cmap)
la función classificationReport retorna las métricas basadas en los resultados de cada modelo.
def classificationReport(y_true, y_pred):
"""
función que entrega el accuracy
"""
originalclass.extend(y_true)
predictedclass.extend(y_pred)
#retorno las métricas accuracy, precision, recall
return accuracy_score(y_true, y_pred)
La función plot_classificationReport recibe como parámetros las métricas del reporte de clasificación basados en el score.
def plot_classificationReport(y_true, y_pred, figsize=(10, 6), ax=None):
"""
función que grafica las métricas precision, recall y f1 score
"""
plt.figure(figsize=figsize)
plt.title('Métricas modelo')
xticks = ['Precision', 'Recall', 'f1-score']
yticks=['No acepta', 'Acepta']
rep = np.array(precision_recall_fscore_support(y_true, y_pred)).T
sns.heatmap(rep[:,:-1], annot=True, cbar=True, xticklabels=xticks, yticklabels=yticks, ax=ax, cmap="RdBu")
La funcion plot_learning_curve recibe como parámetros de entrada el modelo a usar y las variables "X" e "y", retornando el gráfico.
def plot_learning_curve(estimator, X, y, ylim=None, cv=None,
n_jobs=1, train_sizes=np.linspace(.1, 1.0, 5)):
"""
Función que grafica la curva learning rate training score vs cross validation
"""
plt.figure()
if ylim is not None:
plt.ylim(*ylim)
plt.xlabel("Muestras de entrenamiento")
plt.ylabel("Score")
plt.title("Learning Curves")
train_sizes, train_scores, test_scores = learning_curve(
estimator, X, y, cv=cv, n_jobs=n_jobs, train_sizes=train_sizes)
train_scores_mean = np.mean(train_scores, axis=1)
train_scores_std = np.std(train_scores, axis=1)
test_scores_mean = np.mean(test_scores, axis=1)
test_scores_std = np.std(test_scores, axis=1)
plt.grid()
plt.fill_between(train_sizes, train_scores_mean - train_scores_std,
train_scores_mean + train_scores_std, alpha=0.1,
color="r")
plt.fill_between(train_sizes, test_scores_mean - test_scores_std,
test_scores_mean + test_scores_std, alpha=0.1, color="g")
plt.plot(train_sizes, train_scores_mean, 'o-', color="r",
label="Training score")
plt.plot(train_sizes, test_scores_mean, 'o-', color="g",
label="Cross-validation score")
plt.legend(loc="best")
return plt
La función plotROC recibe como parámetros el modelo seleccionado, la variable "X" que son las variables independientes de nuestro modelo y la variable "y" la variable dependiente a predecir.
def plotROC(model, X, y):
"""
Función que grafica la curva de ROC
"""
clf = model
x_train, x_test, y_train, y_test = cross_validation.train_test_split(X, y)
clf.fit(x_train, y_train)
y_pred = clf.predict(x_test)
fpr, tpr, thresholds = roc_curve(y_test, y_pred)
roc_auc = auc(fpr, tpr)
plt.title('Receiver Operating Characteristic')
plt.plot(fpr, tpr, label='AUC = %0.4f'% roc_auc)
plt.legend(loc='lower right')
plt.plot([0,1],[0,1],'r--')
plt.xlim([-0.001, 1])
plt.ylim([0, 1.001])
plt.ylabel('True Positive Rate')
plt.xlabel('False Positive Rate')
plt.show()
Prepararemos los modelos seleccionando un set de parámetros (kernel, regularización, etc.) y para tener el mejor ajuste usaremos la función RandomsearchCV.
5. Constucción y evaluación del modelo¶
#preparo los modelos
param_grid_xgb = {'n_estimators':[50,100,150,200],
'max_depth':[2,3,4,5,6,7,8,9],
'min_child_weight':[2,3,4,5],
'colsample_bytree':[0.2,0.6,0.8],
'colsample_bylevel':[0.2,0.6,0.8]}
param_grid_lr = {'C': [0.001, 0.01, 0.1, 1, 10, 100, 1000] }
#seleccionamos el mejor parámetro para regresión logística
lr=LogisticRegression()
estimacion_lr = RandomizedSearchCV(estimator=lr, n_iter=7, param_distributions=param_grid_lr, cv= 5)
estimacion_lr.fit(X, y)
print (estimacion_lr.best_params_)
#seleccionamos el mejor parámetro para xgboost
xg=XGBClassifier()
estimacion_xgb = RandomizedSearchCV(estimator=xg, param_distributions=param_grid_xgb, cv= 5, n_iter=5)
estimacion_xgb.fit(X, y)
print (estimacion_xgb.best_params_)
#guardo los modelos
modelos = []
modelos.append(('GNB', GaussianNB()))
modelos.append(('LR', LogisticRegression(C=0.001)))
modelos.append(('xbg', XGBClassifier()))
Para evaluar el modelo usaremos como muestra de entrenamiento un 75% y de validación un 25%. Usaremos validación cruzada con 10 iteraciones con muestras al azar por cada iteración.
#evalúo el modelo
resultados=[]
nombres=[]
scoring = 'accuracy'
originalclass = []
predictedclass = []
for nombre, modelo in modelos:
shuffle_split = ShuffleSplit(test_size=.25, n_splits=10, random_state=seed)
cv_resultados = cross_val_score(modelo, X, y, cv=shuffle_split, scoring=make_scorer(classificationReport))
resultados.append(cv_resultados)
nombres.append(nombre)
msg = ("%s %f (%f)" % (nombre, cv_resultados.mean(), cv_resultados.std()))
print(msg)
El resultado de los modelos tenemos que el mejor accuracy lo entrega el modelo de gradient boosting con un 85,6% aproximado de acierto correctos, en segundo lugar se encuentra la regresión logistica y por último el método de Bayes, en este caso el método de boosting es el que seleccionaremos para usar en nuestro modelo, a continuación veremos otras métricas por cada modelo.
#graficamos el acuracy en los 10 k fold-cross validation
plt.plot(resultados[0], label='SVC')
plt.plot(resultados[1], label='LG')
plt.plot(resultados[2], label='XGB')
plt.ylabel('Accuray')
plt.xlabel('Num k fold')
plt.title('Comparación algoritmos / Num k fold')
plt.legend(loc=3)
plt.show()
El gráfico muestra el accuracy y como se comporta en las 10 iteraciones considerando la validación cruzada, además se puede apreciar que el método de Gradient boosting siempre tiene una mejor performance comparando los otros modelos por lo que su capacidad de clasificación es mejor en todas las iteraciones.
nClass=[]
for i in range(0, len(modelos)+1):
nClass.append(len(originalclass)/(len(modelos))*i )
i=+1
#métricas de regresión logística
%matplotlib inline
plot_confusionMatrix(originalclass[int(nClass[0]):int(nClass[1])], predictedclass[int(nClass[0]):int(nClass[1])])
classificationReport=(classification_report(originalclass[int(nClass[0]):int(nClass[1])],
predictedclass[int(nClass[0]):int(nClass[1])]))
print((classificationReport))
plot_classificationReport(originalclass[int(nClass[0]):int(nClass[1])], predictedclass[int(nClass[0]):int(nClass[1])])
Al revisar las métricas de la matriz de confusión del modelo de Bayes, predijo que en 10 iteraciones 86.906 clientes no aceptaron el producto, estos fueron correctamete clasificados es decir son los verdaderos positivos y fueron mal clasificados 2.057 clientes como falso negativo. los verdaderos negativo que son 55.561 clientes clasificados correctamente aceptando el producto y los falsos positivos son 44.352 clientes mal clasificados. la exactitud total del modelo (overall accuracy) corresponde: $(Verdaderos\space positivo + Verdaderos\space negativo)/\space Total$ . Resultando un 74%. resultado que revisamos en el paso anterior al comparar cada modelo.
La otras métricas son precision que es el ratio de como clasificó correctamente observaciones positivas es decir $Verdaderos\space positivos / (Verdaderos\space positivos + Falsos\space positivos)$ en este caso es de 66% para los clientes que no aceptan y un 81% para los clientes que aceptan. Recall es otro ratio que mide los eventos clasificados positivamente como Verdaderos positivos/(Verdaderos positivos+Falsos negativos)Verdaderos positivos/(Verdaderos positivos+Falsos negativos) en este caso es de 87% para los clientes que no aceptan y un 56% para los que aceptan el producto.
#métricas de SVM
plot_confusionMatrix(originalclass[int(nClass[1]):int(nClass[2])], predictedclass[int(nClass[1]):int(nClass[2])])
classificationReport=classification_report(originalclass[int(nClass[1]):int(nClass[2])],
predictedclass[int(nClass[1]):int(nClass[2])])
print(classificationReport)
plot_classificationReport(originalclass[int(nClass[1]):int(nClass[2])], predictedclass[int(nClass[1]):int(nClass[2])])
La matriz de confusión en el modelo de regresión logística, este predijo en 10 iteraciones que 84.886 clientes "no eceptaron" el producto fueron correctamete clasificados es decir son los verdaderos positivos y fueron mal clasificados 14.811 clientes como falso negativo. los verdaderos negativo que son 81.304 clientes clasificados correctamente como "aceptaron" y los falsos positivos son 18.609 clientes mal clasificados. la exactitud total del modelo (overall accuracy) corresponde a 83%. la precision es de 82% para los que no aceptan y 85% para los que aceptan el producto. El recall es de 85% para los que no aceptan y 81% para los que aceptan el producto. El resultado es mucho mejor comparandolo con bayes en especial a nosotros nos interesa que clasifique de mejor manera los clientes que aceptan.
#métricas de bosques aleatorios
plot_confusionMatrix(originalclass[int(nClass[2]):int(nClass[3])], predictedclass[int(nClass[2]):int(nClass[3])])
classificationReport=classification_report(originalclass[int(nClass[2]):int(nClass[3])],
predictedclass[int(nClass[2]):int(nClass[3])])
print(classificationReport)
plot_classificationReport(originalclass[int(nClass[2]):int(nClass[3])], predictedclass[int(nClass[2]):int(nClass[3])])
La matriz de confusión en el modelo de gradian boosting, este predijo en 10 iteraciones que 82.789 clientes "no aceptaron" el prodcuto fueron correctamete clasificados es decir son los verdaderos positivos y fueron mal clasificados 16.908 clientes como falso negativo. los verdaderos negativo que son 88.743 clientes clasificados correctamente como "aceptaron" y los falsos positivos son 11.170 clientes mal clasificados. la exactitud total del modelo (overall accuracy) corresponde a 86%. la precision es de 88% para los que no aceptan y 84% para los que aceptan el producto. El recall es de 83% para los que no aceptan y 89% para los que aceptan el producto. Comparandolo con regresión logistica este modelo tiene una mejor capacidad para clasificar los clientes que "aceptan" el producto y si consideramos el promedio total de la precisión nos quedaremos finalmente con el método de gradian boosting.
#graficamos la curva de aprendizaje
cv = ShuffleSplit(test_size=.25, n_splits=10, random_state=seed)
estimator = modelos[2][1]
estimator.fit(X, y)
plot_learning_curve(estimator, X, y, (0.7, 1.01), cv=cv, n_jobs=4)
plt.show()
El gráfico de la curva de aprendisaje se puede visualizar como se comporta el modelo de gradiant boosting entre el puntaje con la muestra de entrenamiento y el puntaje de validación cruzada, y esta se basa en la cantidad de muestras, se puede apreciar que el modelo es bastante robusto ya que no experimenta mayores cambios en ambas curvas y la distancia entre ambas es muy pequeña por lo que no es necesario realizar algún ajuste (tomar mas muestras de entrenamiento o aplicar alguna técnica para reducir variables).
proba=plotROC(modelos[2][1], X, y)
La curva de ROC, para el modelo final nos entrega el área bajo la curva y este gráfico es la representación de la razón o ratio de verdaderos positivos (VPR = Razón de Verdaderos Positivos) frente a la razón o ratio de falsos positivos (FPR = Razón de Falsos Positivos) así tenemos el umbral de clasificación de un 85.8%, este resultado significa la capacidad para discriminar entre las dos clases que tenemos y es mucho mejor que dejarlo al azar que sería equivalente a un 50%.
6. Conclusión¶
Para concluir, luego del análisis exploratorio de las variables, la codificación de las etiquetas y el uso de técnicas como one hot encoding para usarlo en modelos como regresión logística tenemos un resultado muy bueno ya que nuestro modelo tiene la capacidad de predicción del 85.8%, luego de balancear la variable a predecir y de probar diferentes modelos, el mejor resultado nos da el modelo de Gradient boosting con el que finalmente nos quedamos y utilizaremos.